/**
 * Copyright 2007 DFKI GmbH.
 * All Rights Reserved.  Use is subject to license terms.
 *
 * This file is part of MARY TTS.
 *
 * MARY TTS is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, version 3 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
package marytts.server.http;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;

import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;

import marytts.datatypes.MaryDataType;
import marytts.modules.synthesis.Voice;
import marytts.server.Request;
import marytts.server.RequestHandler.StreamingOutputPiper;
import marytts.server.RequestHandler.StreamingOutputWriter;
import marytts.util.MaryRuntimeUtils;
import marytts.util.MaryUtils;
import marytts.util.data.audio.MaryAudioUtils;
import marytts.util.http.Address;

import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.logging.log4j.Logger;

/**
 * Provides functionality to process synthesis http requests
 * 
 * @author Oytun T&uuml;rk
 *
 */
public class SynthesisRequestHandler extends BaseHttpRequestHandler {
	private static int id = 0;

	private static synchronized int getId() {
		return id++;
	}

	private StreamingOutputWriter outputToStream;
	private StreamingOutputPiper streamToPipe;
	private PipedOutputStream pipedOutput;
	private PipedInputStream pipedInput;

	public SynthesisRequestHandler() {
		super();

		outputToStream = null;
		streamToPipe = null;
		pipedOutput = null;
		pipedInput = null;
	}

	@Override
	protected void handleClientRequest(String absPath, Map<String, String> queryItems, HttpResponse response,
			Address serverAddressAtClient) throws IOException {
		/*
		 * response.setStatusCode(HttpStatus.SC_OK); TestProducingNHttpEntity entity = new TestProducingNHttpEntity();
		 * entity.setContentType("audio/x-mp3"); response.setEntity(entity); if (true) return;
		 */
		logger.debug("New synthesis request: " + absPath);
		if (queryItems != null) {
			for (String key : queryItems.keySet()) {
				logger.debug("    " + key + "=" + queryItems.get(key));
			}
		}
		process(serverAddressAtClient, queryItems, response);

	}

	public void process(Address serverAddressAtClient, Map<String, String> queryItems, HttpResponse response) {
		if (queryItems == null
				|| !(queryItems.containsKey("INPUT_TYPE") && queryItems.containsKey("OUTPUT_TYPE")
						&& queryItems.containsKey("LOCALE") && queryItems.containsKey("INPUT_TEXT"))) {
			MaryHttpServerUtils.errorMissingQueryParameter(response,
					"'INPUT_TEXT' and 'INPUT_TYPE' and 'OUTPUT_TYPE' and 'LOCALE'");
			return;
		}

		String inputText = queryItems.get("INPUT_TEXT");

		MaryDataType inputType = MaryDataType.get(queryItems.get("INPUT_TYPE"));
		if (inputType == null) {
			MaryHttpServerUtils.errorWrongQueryParameterValue(response, "INPUT_TYPE", queryItems.get("INPUT_TYPE"), null);
			return;
		}

		MaryDataType outputType = MaryDataType.get(queryItems.get("OUTPUT_TYPE"));
		if (outputType == null) {
			MaryHttpServerUtils.errorWrongQueryParameterValue(response, "OUTPUT_TYPE", queryItems.get("OUTPUT_TYPE"), null);
			return;
		}
		boolean isOutputText = true;
		boolean streamingAudio = false;
		AudioFileFormat.Type audioFileFormatType = null;
		if (outputType.name().contains("AUDIO")) {
			isOutputText = false;
			String audioTypeName = queryItems.get("AUDIO");
			if (audioTypeName == null) {
				MaryHttpServerUtils.errorMissingQueryParameter(response, "'AUDIO' when OUTPUT_TYPE=AUDIO");
				return;
			}
			if (audioTypeName.endsWith("_STREAM")) {
				streamingAudio = true;
			}
			int lastUnderscore = audioTypeName.lastIndexOf('_');
			if (lastUnderscore != -1) {
				audioTypeName = audioTypeName.substring(0, lastUnderscore);
			}
			try {
				audioFileFormatType = MaryAudioUtils.getAudioFileFormatType(audioTypeName);
			} catch (Exception ex) {
			}
			if (audioFileFormatType == null) {
				MaryHttpServerUtils.errorWrongQueryParameterValue(response, "AUDIO", queryItems.get("AUDIO"), null);
				return;
			} else if (audioFileFormatType.toString().equals("MP3") && !MaryRuntimeUtils.canCreateMP3()) {
				MaryHttpServerUtils.errorWrongQueryParameterValue(response, "AUDIO", queryItems.get("AUDIO"),
						"Conversion to MP3 not supported.");
				return;
			} else if (audioFileFormatType.toString().equals("Vorbis") && !MaryRuntimeUtils.canCreateOgg()) {
				MaryHttpServerUtils.errorWrongQueryParameterValue(response, "AUDIO", queryItems.get("AUDIO"),
						"Conversion to OGG Vorbis format not supported.");
				return;
			}
		}
		// optionally, there may be output type parameters
		// (e.g., the list of features to produce for the output type TARGETFEATURES)
		String outputTypeParams = queryItems.get("OUTPUT_TYPE_PARAMS");

		Locale locale = MaryUtils.string2locale(queryItems.get("LOCALE"));
		if (locale == null) {
			MaryHttpServerUtils.errorWrongQueryParameterValue(response, "LOCALE", queryItems.get("LOCALE"), null);
			return;
		}

		Voice voice = null;
		String voiceName = queryItems.get("VOICE");
		if (voiceName != null) {
			if (voiceName.equals("male") || voiceName.equals("female")) {
				voice = Voice.getVoice(locale, new Voice.Gender(voiceName));
			} else {
				voice = Voice.getVoice(voiceName);
			}
			if (voice == null) {
				// a voice name was given but there is no such voice
				MaryHttpServerUtils.errorWrongQueryParameterValue(response, "VOICE", queryItems.get("VOICE"), null);
				return;
			}
		}
		if (voice == null) { // no voice tag -- use locale default if it exists.
			voice = Voice.getDefaultVoice(locale);
			logger.debug("No voice requested -- using default " + voice);
		}

		String style = queryItems.get("STYLE");
		if (style == null)
			style = "";

		String effects = toRequestedAudioEffectsString(queryItems);
		if (effects.length() > 0)
			logger.debug("Audio effects requested: " + effects);
		else
			logger.debug("No audio effects requested");

		String logMsg = queryItems.get("LOG");
		if (logMsg != null) {
			logger.info("Connection info: " + logMsg);
		}

		// Now, the parse is complete.

		// Construct audio file format -- even when output is not AUDIO,
		// in case we need to pass via audio to get our output type.
		if (audioFileFormatType == null) {
			audioFileFormatType = AudioFileFormat.Type.AU;
		}
		AudioFormat audioFormat;
		if (audioFileFormatType.toString().equals("MP3")) {
			audioFormat = MaryRuntimeUtils.getMP3AudioFormat();
		} else if (audioFileFormatType.toString().equals("Vorbis")) {
			audioFormat = MaryRuntimeUtils.getOggAudioFormat();
		} else if (voice != null) {
			audioFormat = voice.dbAudioFormat();
		} else {
			audioFormat = Voice.AF16000;
		}
		AudioFileFormat audioFileFormat = new AudioFileFormat(audioFileFormatType, audioFormat, AudioSystem.NOT_SPECIFIED);

		final Request maryRequest = new Request(inputType, outputType, locale, voice, effects, style, getId(), audioFileFormat,
				streamingAudio, outputTypeParams);

		// Process the request and send back the data
		boolean ok = true;
		try {
			maryRequest.setInputData(inputText);
			logger.info("Read: " + inputText);
		} catch (Exception e) {
			String message = "Problem reading input";
			logger.warn(message, e);
			MaryHttpServerUtils.errorInternalServerError(response, message, e);
			ok = false;
		}
		if (ok) {
			if (streamingAudio) {
				// Start two separate threads:
				// 1. one thread to process the request;
				new Thread("RH " + maryRequest.getId()) {
					public void run() {
						Logger myLogger = MaryUtils.getLogger(this.getName());
						try {
							maryRequest.process();
							myLogger.info("Streaming request processed successfully.");
						} catch (Throwable t) {
							myLogger.error("Processing failed.", t);
						}
					}
				}.start();

				// 2. one thread to take the audio data as it becomes available
				// and write it into the ProducingNHttpEntity.
				// The second one does not depend on the first one practically,
				// because the AppendableSequenceAudioInputStream returned by
				// maryRequest.getAudio() was already created in the constructor of Request.
				AudioInputStream audio = maryRequest.getAudio();
				assert audio != null : "Streaming audio but no audio stream -- very strange indeed! :-(";
				AudioFileFormat.Type audioType = maryRequest.getAudioFileFormat().getType();
				AudioStreamNHttpEntity entity = new AudioStreamNHttpEntity(maryRequest);
				new Thread(entity, "HTTPWriter " + maryRequest.getId()).start();
				// entity knows its contentType, no need to set explicitly here.
				response.setEntity(entity);
				response.setStatusCode(HttpStatus.SC_OK);
				return;
			} else { // not streaming audio
				// Process input data to output data
				try {
					maryRequest.process(); // this may take some time
				} catch (Throwable e) {
					String message = "Processing failed.";
					logger.error(message, e);
					MaryHttpServerUtils.errorInternalServerError(response, message, e);
					ok = false;
				}
				if (ok) {
					// Write output data to client
					try {
						ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
						maryRequest.writeOutputData(outputStream);
						String contentType;
						if (maryRequest.getOutputType().isXMLType() || maryRequest.getOutputType().isTextType()) // text output
							contentType = "text/plain; charset=UTF-8";
						else
							// audio output
							contentType = MaryHttpServerUtils.getMimeType(maryRequest.getAudioFileFormat().getType());
						MaryHttpServerUtils.toHttpResponse(outputStream.toByteArray(), response, contentType);
					} catch (Exception e) {
						String message = "Cannot write output";
						logger.warn(message, e);
						MaryHttpServerUtils.errorInternalServerError(response, message, e);
						ok = false;
					}
				}
			}
		}

		if (ok)
			logger.info("Request handled successfully.");
		else
			logger.info("Request couldn't be handled successfully.");
		if (MaryRuntimeUtils.lowMemoryCondition()) {
			logger.info("Low memory condition detected (only " + MaryUtils.availableMemory()
					+ " bytes left). Triggering garbage collection.");
			Runtime.getRuntime().gc();
			logger.info("After garbage collection: " + MaryUtils.availableMemory() + " bytes available.");
		}
	}

	protected String toRequestedAudioEffectsString(Map<String, String> keyValuePairs) {
		StringBuilder effects = new StringBuilder();
		StringTokenizer tt;
		Set<String> keys = keyValuePairs.keySet();
		String currentKey;
		String currentEffectName, currentEffectParams;
		for (Iterator<String> it = keys.iterator(); it.hasNext();) {
			currentKey = it.next();
			if (currentKey.startsWith("effect_")) {
				if (currentKey.endsWith("_selected")) {
					if (keyValuePairs.get(currentKey).compareTo("on") == 0) {
						if (effects.length() > 0)
							effects.append("+");

						tt = new StringTokenizer(currentKey, "_");
						if (tt.hasMoreTokens())
							tt.nextToken(); // Skip "effects_"
						if (tt.hasMoreTokens()) // The next token is the effect name
						{
							currentEffectName = tt.nextToken();

							currentEffectParams = keyValuePairs.get("effect_" + currentEffectName + "_parameters");
							if (currentEffectParams != null && currentEffectParams.length() > 0)
								effects.append(currentEffectName).append("(").append(currentEffectParams).append(")");
							else
								effects.append(currentEffectName);
						}
					}
				}
			}
		}

		return effects.toString();
	}

}
